<script setup name="TreeView" lang="ts">
import { reactive, watch, ref, onMounted, shallowRef, nextTick } from "vue";

export interface TreeNode {
  // 显示标题
  title: string;
  // 下一级
  children?: TreeNode[];
}

/**
 * 新增三个类型
 * NodePosition： 定义一个单位的位置信息和宽高还有类型
 * 'text'类型为文本节点，'block'类型为区块节点
 *
 * TextNodePosition：文本节点，继承NodePosition
 *
 * BlockNodePosition：区块节点，继承NodePosition，title 属性为这个区块的头部节点
 */
export interface NodePosition {
  startX: number;

  startY: number;

  height: number;

  width: number;

  type: "text" | "block";
}

export interface TextPosition extends NodePosition {
  type: "text";
  // 文本节点内容
  title: string;
}

export interface BlockPosition extends NodePosition {
  type: "block";
  // 区块节点头部的文本，以杭州市为例，"杭州市"这三个字作为一个文本节点，放在title上
  title: TextPosition;
  // “拱墅区”、“余杭区”作为子元素，放在children里面
  children: NodePosition[];
}

const canvasRef = shallowRef<HTMLCanvasElement>();

const canvasContext = shallowRef<CanvasRenderingContext2D | null>();

function getCtx(): CanvasRenderingContext2D {
  return canvasContext.value!;
}

const tree: TreeNode = {
  title: "浙江省",
  children: [
    {
      title: "杭州市",
      children: [
        {
          title: "拱墅区",
          children: [
            {
              title: "北部软件园",
            },
          ],
        },
        {
          title: "余杭区",
          children: [
            {
              title: "梦想小镇",
            },
            {
              title: "欧美金融城",
              children:[
                {
                  title:"无",
                  children:[
                    {
                      title:"xixi"
                    },
                    {
                      title:"xixi2"
                    }
                  ]
                }
              ]
            },
          ],
        },
      ],
    },
    {
      title: "宁波市",
      children: [
        {
          title: "海曙区",
        },
        {
          title: "江北区",
        },
      ],
    },
  ],
};

onMounted(() => {
  canvasContext.value = canvasRef.value?.getContext("2d");
  nextTick(() => {
    const node = renderNode(
      {
        startX: 10,
        startY: 10,
      },
      tree
    );
    shakeTree(node);
    draw(node);
  });
});

// 绘制一个节点需要的高度
const nodeHeight = 50;
// 绘制一个节点需要的宽度
const nodeWidth = 100;
// 节点之间留出来点垂直方向的间隙
const nodeMarginY = 18;
// 节点之间留出来点水平方向的间隙
const nodeMarginX = 10;

// 判断是否文本节点,告诉编辑器e是TextPosition类型
// 用的时候就不需要 e as TextPosition 强转
function isTextNode(e: NodePosition): e is TextPosition {
  return e.type === "text";
}

// 判断是否块本节点,告诉编辑器e是BlockPosition类型
// 用的时候就不需要 e as BlockPosition 强转
function isBlockNode(e: NodePosition): e is BlockPosition {
  return e.type === "block";
}

// 摇树一下
function shakeTree(nodePosition: NodePosition) {
  // 普通文本节点没有居中的必要，直接返回坐标
  if (isTextNode(nodePosition)) {
    return nodePosition.startX;
  }

  // 块节点，要对头部的文本节点进行居中操作
  const blockNode = nodePosition as BlockPosition;

  // 保存子节点第一个和最后一个的居中StarX
  let firstChildCenterStarX = 0,
    lastChildCenterStartX = 0;

  // 遍历递归子节点
  blockNode.children.forEach((e, index) => {
    // 先摇动子节点，让最下层的子节点完成居中，然后依次处理上层
    const centerStartX = shakeTree(e);

    if (index === 0) {
      firstChildCenterStarX = centerStartX;
    }

    if (index === blockNode.children.length - 1) {
      lastChildCenterStartX = centerStartX;
    }
  });

  // 区块标题的居中点
  // 即 `最后一个子节点` 与 `第一个子节点的位置` 这段距离 的 `中间位置`
  const newStartX =
    (lastChildCenterStartX - firstChildCenterStarX) / 2 + firstChildCenterStarX;

  // 赋值给区块的头部文本
  blockNode.title.startX = newStartX;

  // 将这个区块的头部文本节点的居中位置返回给上层
  // 上层拿到后再做上层的居中操作
  return newStartX;
}

interface StartPosition {
  startX: number;
  startY: number;
}

function renderNode(
  startPosition: StartPosition,
  treeNode: TreeNode
): TextPosition | BlockPosition {
  // 有子节点，定义为block节点
  if (treeNode.children && treeNode.children.length > 0) {
    // 区块宽度，累加
    let blockWidth = 0;

    // 区块高度，取子节点最大值
    // 这里给一个默认的值
    let blockHeight = nodeHeight + nodeMarginY + nodeHeight;

    // 画节点时保存下一个节点的起始位置
    let lastItemPosition = {
      // 第一个子节点，起始 startX 就是上层传进来的 startX
      nextStartX: startPosition.startX,
      // 子节点的起始 startY 应该预留出头部的空间
      // 比如 杭州市的块节点，拱墅区和余杭区的 startY 应该在文本节点 “杭州市”下面
      nextStartY: startPosition.startY + nodeHeight + nodeMarginY,
    };

    // 存储子节点的数组
    const children: NodePosition[] = [];

    // 遍历子节点
    treeNode.children.forEach((e) => {
      // 递归生成子节点的位置信息，拿到位置信息去处理下一个子节点
      const childPosition = renderNode(
        {
          startX: lastItemPosition.nextStartX,
          startY: lastItemPosition.nextStartY,
        },
        e
      );

      // 这里取所有子节点的最大高度，作为区块的高度
      if (childPosition.height > blockHeight) {
        blockHeight = childPosition.height;
      }

      // 累加子节点宽度，作为区块宽度
      // 这里最后会多加出来一个nodeMarginX，最后要减去
      blockWidth += childPosition.width + nodeMarginX;

      // 给下一个兄弟节点准备起始位置
      lastItemPosition = {
        // 兄弟节点的startY都是一样的，不需要动
        nextStartY: childPosition.startY,
        // 兄弟节点的startX要累加前当前的宽度，再预留出点边距
        nextStartX: childPosition.startX + childPosition.width + nodeMarginX,
      };

      // 将位置信息添加到数组
      children.push(childPosition);
    });

    // 返回数据给上一层
    return {
      startX: startPosition.startX,
      startY: startPosition.startY,
      type: "block",
      // 减去一个多余的nodeMarginX
      width: blockWidth - nodeMarginX,
      height: blockHeight,
      children: children,
      title: {
        startX: startPosition.startX,
        startY: startPosition.startY,
        type: "text",
        title: treeNode.title,
        width: nodeWidth,
        height: nodeHeight,
      },
    };
  }

  // 没有子节点，就是普通的文本节点，直接返回
  return {
    startX: startPosition.startX,
    startY: startPosition.startY,
    type: "text",
    width: nodeWidth,
    height: nodeHeight,
    title: treeNode.title,
  };
}

/**
 * 绘制node树
 */
function draw(node: NodePosition) {
  // 文本节点直接绘制
  if (isTextNode(node)) {
    drawTextItem(node as TextPosition);
  } else if (isBlockNode(node)) {
    // const blockNode = node as BlockPosition;
    // 区块节点，先绘制区块头部的文本节点
    drawTextItem(node.title);
    // 再绘制子节点
    node.children.forEach((e) => {
      draw(e);
    });
    // 主要是块头部画到子节点
    drawLine(node);
  }
}

function drawLine(block: BlockPosition) {
  const ctx = getCtx();

  ctx.beginPath();

  // 计算画线起点
  // startX，是区块头部文本节点的中心位置
  const lineStartX = block.title.startX + block.title.width / 2;

  // startY，是区块头部文本节点的底部
  const lineStartY = block.title.startY + block.title.height;

  // Y转折点，如果有多个子节点，需要在中间分叉
  const halfY = lineStartY + nodeMarginY / 2;

  // 画笔移动起点
  ctx.moveTo(lineStartX, lineStartY);

  // 先把线画到转折点
  ctx.lineTo(lineStartX, halfY);

  // 把转折点和子元素连接起来
  block.children.forEach((e) => {
    // 画完一个子节点后,需要重新回到分叉点,第一次不需要
    ctx.moveTo(lineStartX, halfY);

    // 如果是文本节点
    if (isTextNode(e)) {
      // 直接去文本中心位置
      const childCenterX = e.startX + e.width / 2;
      // 画个转折点
      ctx.lineTo(childCenterX, halfY);
      // 画到文本上方
      ctx.lineTo(childCenterX, e.startY);
      return;
    }

    // 如果是块节点
    if (isBlockNode(e)) {
      // 用上面的方法判断类型,可以隐形的让 e 转为 BlockPosition 类型,这里可以直接取title了
      const childTitle = e.title;
      // 取到头部的中心点位置
      const childTitleCenterX = childTitle.startX + childTitle.width / 2;
      // 画个转折点
      ctx.lineTo(childTitleCenterX, halfY);
      // 画到头部文本上方
      ctx.lineTo(childTitleCenterX, childTitle.startY);
    }
  });
  // 最后别忘了stroke
  ctx.stroke();
}

function drawTextItem({
  startX,
  startY,
  width,
  height,
  title,
}: {
  startX: number;
  startY: number;
  width: number;
  height: number;
  title: string;
}) {
  const ctx = getCtx();

  ctx.beginPath();

  const textWidth = ctx.measureText(title);

  if (width < textWidth.width + 20) {
    const newWidth = textWidth.width + 20;
    width = newWidth;
    startX = startX - (newWidth - width) / 2;
  }

  ctx.rect(startX, startY, width, height);
  ctx.strokeStyle = "red";
  ctx.fillStyle = "pink";

  ctx.stroke();
  ctx.fill();
  ctx.fillStyle = "red";
  ctx.textBaseline = "middle";

  const textXStart = startX + width / 2 - textWidth.width / 2;
  const textYStart = startY + height / 2;

  ctx.fillText(title, textXStart, textYStart, width - 20);
}
</script>

<template>
  <div class="col">
    <canvas ref="canvasRef" id="canvas" width="800" height="800">
      你的浏览器不支持canvas.
    </canvas>
  </div>
</template>
